/**
 * $Id: StencilShape.java,v 1.12 2012-01-13 12:42:10 david Exp $
 * Copyright (c) 2010-2012, JGraph Ltd
 */
package com.kg.rails.component.editor.shape;

import com.kg.rails.component.editor.canvas.Graphics2DCanvas;
import com.kg.rails.component.editor.util.EditorUtils;
import com.kg.rails.component.editor.util.XmlUtils;
import com.kg.rails.component.editor.util.svg.*;
import com.kg.rails.component.editor.view.CellState;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.awt.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

/**
 * Stencil shape drawing that takes an XML definition of the shape and renders
 * it.
 * <p/>
 * See http://projects.gnome.org/dia/custom-shapes for specs. See
 * http://dia-installer.de/shapes_de.html for shapes.
 */
public class StencilShape extends BasicShape {
    public StencilShape() {
        super();
    }

    protected GeneralPath shapePath;

    /**
     * Reference to the root node of the Dia shape description.
     */
    protected Node root;

    protected svgShape rootShape;

    protected Rectangle2D boundingBox;

    protected String name;

    protected String iconPath;

    /**
     * Transform cached to save instance created. Used to scale the internal
     * path of shapes where possible
     */
    protected AffineTransform cachedTransform = new AffineTransform();

    /**
     * Constructs a new stencil for the given Dia shape description.
     */
    public StencilShape(String shapeXml) {
        this(XmlUtils.parseXml(shapeXml));
    }

    public StencilShape(Document document) {
        if (document != null) {
            NodeList nameList = document.getElementsByTagName("name");

            if (nameList != null && nameList.getLength() > 0) {
                this.name = nameList.item(0).getTextContent();
            }

            NodeList iconList = document.getElementsByTagName("icon");

            if (iconList != null && iconList.getLength() > 0) {
                this.iconPath = iconList.item(0).getTextContent();
            }

            NodeList svgList = document.getElementsByTagName("svg:svg");

            if (svgList != null && svgList.getLength() > 0) {
                this.root = svgList.item(0);
            } else {
                svgList = document.getElementsByTagName("svg");

                if (svgList != null && svgList.getLength() > 0) {
                    this.root = svgList.item(0);
                }
            }

            if (this.root != null) {
                rootShape = new svgShape(null, null);
                createShape(this.root, rootShape);
            }
        }
    }

    /**
     *
     */
    @Override
    public void paintShape(Graphics2DCanvas canvas, CellState state) {
        double x = state.getX();
        double y = state.getY();
        double w = state.getWidth();
        double h = state.getHeight();

        canvas.getGraphics().translate(x, y);
        double widthRatio = 1;
        double heightRatio = 1;

        if (boundingBox != null) {
            widthRatio = w / boundingBox.getWidth();
            heightRatio = h / boundingBox.getHeight();
        }

        this.paintNode(canvas, state, rootShape, widthRatio, heightRatio);

        canvas.getGraphics().translate(-x, -y);
    }

    /**
     *
     */
    public void paintNode(Graphics2DCanvas canvas, CellState state,
                          svgShape shape, double widthRatio, double heightRatio) {
        Shape associatedShape = shape.shape;

        boolean fill = false;
        boolean stroke = true;
        Color fillColor = null;
        Color strokeColor = null;

        Map<String, Object> style = shape.style;

        if (style != null) {
            String fillStyle = EditorUtils.getString(style,
                    CSSConstants.CSS_FILL_PROPERTY);
            String strokeStyle = EditorUtils.getString(style,
                    CSSConstants.CSS_STROKE_PROPERTY);

            if (strokeStyle != null
                    && strokeStyle.equals(CSSConstants.CSS_NONE_VALUE)) {
                if (strokeStyle.equals(CSSConstants.CSS_NONE_VALUE)) {
                    stroke = false;
                } else if (strokeStyle.trim().startsWith("#")) {
                    int hashIndex = strokeStyle.indexOf("#");
                    strokeColor = EditorUtils.parseColor(strokeStyle
                            .substring(hashIndex + 1));
                }
            }

            if (fillStyle != null) {
                if (fillStyle.equals(CSSConstants.CSS_NONE_VALUE)) {
                    fill = false;
                } else if (fillStyle.trim().startsWith("#")) {
                    int hashIndex = fillStyle.indexOf("#");
                    fillColor = EditorUtils.parseColor(fillStyle
                            .substring(hashIndex + 1));
                    fill = true;
                } else {
                    fill = true;
                }
            }
        }

        if (associatedShape != null) {
            boolean wasScaled = false;

            if (widthRatio != 1 || heightRatio != 1) {
                transformShape(associatedShape, 0.0, 0.0, widthRatio,
                        heightRatio);
                wasScaled = true;
            }

            // Paints the background
            if (fill && configureGraphics(canvas, state, true)) {
                if (fillColor != null) {
                    canvas.getGraphics().setColor(fillColor);
                }

                canvas.getGraphics().fill(associatedShape);
            }

            // Paints the foreground
            if (stroke && configureGraphics(canvas, state, false)) {
                if (strokeColor != null) {
                    canvas.getGraphics().setColor(strokeColor);
                }

                canvas.getGraphics().draw(associatedShape);
            }

            if (wasScaled) {
                transformShape(associatedShape, 0.0, 0.0, 1.0 / widthRatio,
                        1.0 / heightRatio);
            }
        }

        /*
           * If root is a group element, then we should add it's styles to the
           * children.
           */
        for (svgShape subShape : shape.subShapes) {
            paintNode(canvas, state, subShape, widthRatio, heightRatio);
        }
    }

    /**
     * Scales the points composing this shape by the x and y ratios specified
     *
     * @param shape       the shape to scale
     * @param transX      the x translation
     * @param transY      the y translation
     * @param widthRatio  the x co-ordinate scale
     * @param heightRatio the y co-ordinate scale
     */
    protected void transformShape(Shape shape, double transX, double transY,
                                  double widthRatio, double heightRatio) {
        if (shape instanceof Rectangle2D) {
            Rectangle2D rect = (Rectangle2D) shape;
            if (transX != 0 || transY != 0) {
                rect.setFrame(rect.getX() + transX, rect.getY() + transY,
                        rect.getWidth(), rect.getHeight());
            }

            if (widthRatio != 1 || heightRatio != 1) {
                rect.setFrame(rect.getX() * widthRatio, rect.getY()
                        * heightRatio, rect.getWidth() * widthRatio,
                        rect.getHeight() * heightRatio);
            }
        } else if (shape instanceof Line2D) {
            Line2D line = (Line2D) shape;
            if (transX != 0 || transY != 0) {
                line.setLine(line.getX1() + transX, line.getY1() + transY,
                        line.getX2() + transX, line.getY2() + transY);
            }
            if (widthRatio != 1 || heightRatio != 1) {
                line.setLine(line.getX1() * widthRatio, line.getY1()
                        * heightRatio, line.getX2() * widthRatio, line.getY2()
                        * heightRatio);
            }
        } else if (shape instanceof GeneralPath) {
            GeneralPath path = (GeneralPath) shape;
            cachedTransform.setToScale(widthRatio, heightRatio);
            cachedTransform.translate(transX, transY);
            path.transform(cachedTransform);
        } else if (shape instanceof ExtendedGeneralPath) {
            ExtendedGeneralPath path = (ExtendedGeneralPath) shape;
            cachedTransform.setToScale(widthRatio, heightRatio);
            cachedTransform.translate(transX, transY);
            path.transform(cachedTransform);
        } else if (shape instanceof Ellipse2D) {
            Ellipse2D ellipse = (Ellipse2D) shape;
            if (transX != 0 || transY != 0) {
                ellipse.setFrame(ellipse.getX() + transX, ellipse.getY()
                        + transY, ellipse.getWidth(), ellipse.getHeight());
            }
            if (widthRatio != 1 || heightRatio != 1) {
                ellipse.setFrame(ellipse.getX() * widthRatio, ellipse.getY()
                        * heightRatio, ellipse.getWidth() * widthRatio,
                        ellipse.getHeight() * heightRatio);
            }
        }
    }

    /**
     *
     */
    public void createShape(Node root, svgShape shape) {
        Node child = root.getFirstChild();
        /*
           * If root is a group element, then we should add it's styles to the
           * childrens...
           */
        while (child != null) {
            if (isGroup(child.getNodeName())) {
                String style = ((Element) root).getAttribute("style");
                Map<String, Object> styleMap = StencilShape
                        .getStylenames(style);
                svgShape subShape = new svgShape(null, styleMap);
                createShape(child, subShape);
            }

            svgShape subShape = createElement(child);

            if (subShape != null) {
                shape.subShapes.add(subShape);
            }
            child = child.getNextSibling();
        }

        for (svgShape subShape : shape.subShapes) {
            if (subShape != null && subShape.shape != null) {
                if (boundingBox == null) {
                    boundingBox = subShape.shape.getBounds2D();
                } else {
                    boundingBox.add(subShape.shape.getBounds2D());
                }
            }
        }

        // If the shape does not butt up against either or both axis,
        // ensure it is flush against both
        if (boundingBox != null
                && (boundingBox.getX() != 0 || boundingBox.getY() != 0)) {
            for (svgShape subShape : shape.subShapes) {
                if (subShape != null && subShape.shape != null) {
                    transformShape(subShape.shape, -boundingBox.getX(),
                            -boundingBox.getY(), 1.0, 1.0);
                }
            }
        }
    }

    /**
     * Forms an internal representation of the specified SVG element and returns
     * that representation
     *
     * @param root the SVG element to represent
     * @return the internal representation of the element, or null if an error
     *         occurs
     */
    public svgShape createElement(Node root) {
        Element element = null;

        if (root instanceof Element) {
            element = (Element) root;
            String style = element.getAttribute("style");
            Map<String, Object> styleMap = StencilShape.getStylenames(style);

            if (isRectangle(root.getNodeName())) {
                svgShape rectShape = null;

                try {
                    String xString = element.getAttribute("x");
                    String yString = element.getAttribute("y");
                    String widthString = element.getAttribute("width");
                    String heightString = element.getAttribute("height");

                    // Values default to zero if not specified
                    double x = 0;
                    double y = 0;
                    double width = 0;
                    double height = 0;

                    if (xString.length() > 0) {
                        x = Double.valueOf(xString);
                    }
                    if (yString.length() > 0) {
                        y = Double.valueOf(yString);
                    }
                    if (widthString.length() > 0) {
                        width = Double.valueOf(widthString);
                        if (width < 0) {
                            return null; // error in SVG spec
                        }
                    }
                    if (heightString.length() > 0) {
                        height = Double.valueOf(heightString);
                        if (height < 0) {
                            return null; // error in SVG spec
                        }
                    }

                    String rxString = element.getAttribute("rx");
                    String ryString = element.getAttribute("ry");
                    double rx = 0;
                    double ry = 0;

                    if (rxString.length() > 0) {
                        rx = Double.valueOf(rxString);
                        if (rx < 0) {
                            return null; // error in SVG spec
                        }
                    }
                    if (ryString.length() > 0) {
                        ry = Double.valueOf(ryString);
                        if (ry < 0) {
                            return null; // error in SVG spec
                        }
                    }

                    if (rx > 0 || ry > 0) {
                        // Specification rules on rx and ry
                        if (rx > 0 && ryString.length() == 0) {
                            ry = rx;
                        } else if (ry > 0 && rxString.length() == 0) {
                            rx = ry;
                        }
                        if (rx > width / 2.0) {
                            rx = width / 2.0;
                        }
                        if (ry > height / 2.0) {
                            ry = height / 2.0;
                        }

                        rectShape = new svgShape(new RoundRectangle2D.Double(x,
                                y, width, height, rx, ry), styleMap);
                    } else {
                        rectShape = new svgShape(new Rectangle2D.Double(x, y,
                                width, height), styleMap);
                    }
                } catch (Exception e) {
                    // TODO log something useful
                }

                return rectShape;
            } else if (isLine(root.getNodeName())) {
                String x1String = element.getAttribute("x1");
                String x2String = element.getAttribute("x2");
                String y1String = element.getAttribute("y1");
                String y2String = element.getAttribute("y2");

                double x1 = 0;
                double x2 = 0;
                double y1 = 0;
                double y2 = 0;

                if (x1String.length() > 0) {
                    x1 = Double.valueOf(x1String);
                }
                if (x2String.length() > 0) {
                    x2 = Double.valueOf(x2String);
                }
                if (y1String.length() > 0) {
                    y1 = Double.valueOf(y1String);
                }
                if (y2String.length() > 0) {
                    y2 = Double.valueOf(y2String);
                }

                svgShape lineShape = new svgShape(new Line2D.Double(x1, y1, x2,
                        y2), styleMap);
                return lineShape;
            } else if (isPolyline(root.getNodeName())
                    || isPolygon(root.getNodeName())) {
                String pointsString = element.getAttribute("points");
                Shape shape;

                if (isPolygon(root.getNodeName())) {
                    shape = AWTPolygonProducer.createShape(pointsString,
                            GeneralPath.WIND_NON_ZERO);
                } else {
                    shape = AWTPolylineProducer.createShape(pointsString,
                            GeneralPath.WIND_NON_ZERO);
                }

                if (shape != null) {
                    return new svgShape(shape, styleMap);
                }

                return null;
            } else if (isCircle(root.getNodeName())) {
                double cx = 0;
                double cy = 0;
                double r = 0;

                String cxString = element.getAttribute("cx");
                String cyString = element.getAttribute("cy");
                String rString = element.getAttribute("r");

                if (cxString.length() > 0) {
                    cx = Double.valueOf(cxString);
                }
                if (cyString.length() > 0) {
                    cy = Double.valueOf(cyString);
                }
                if (rString.length() > 0) {
                    r = Double.valueOf(rString);

                    if (r < 0) {
                        return null; // error in SVG spec
                    }
                }

                return new svgShape(new Ellipse2D.Double(cx - r, cy - r, r * 2,
                        r * 2), styleMap);
            } else if (isEllipse(root.getNodeName())) {
                double cx = 0;
                double cy = 0;
                double rx = 0;
                double ry = 0;

                String cxString = element.getAttribute("cx");
                String cyString = element.getAttribute("cy");
                String rxString = element.getAttribute("rx");
                String ryString = element.getAttribute("ry");

                if (cxString.length() > 0) {
                    cx = Double.valueOf(cxString);
                }
                if (cyString.length() > 0) {
                    cy = Double.valueOf(cyString);
                }
                if (rxString.length() > 0) {
                    rx = Double.valueOf(rxString);

                    if (rx < 0) {
                        return null; // error in SVG spec
                    }
                }
                if (ryString.length() > 0) {
                    ry = Double.valueOf(ryString);

                    if (ry < 0) {
                        return null; // error in SVG spec
                    }
                }

                return new svgShape(new Ellipse2D.Double(cx - rx, cy - ry,
                        rx * 2, ry * 2), styleMap);
            } else if (isPath(root.getNodeName())) {
                String d = element.getAttribute("d");
                Shape pathShape = AWTPathProducer.createShape(d,
                        GeneralPath.WIND_NON_ZERO);
                return new svgShape(pathShape, styleMap);
            }
        }

        return null;
    }

    /*
      *
      */
    private boolean isRectangle(String tag) {
        return tag.equals("svg:rect") || tag.equals("rect");
    }

    /*
      *
      */
    private boolean isPath(String tag) {
        return tag.equals("svg:path") || tag.equals("path");
    }

    /*
      *
      */
    private boolean isEllipse(String tag) {
        return tag.equals("svg:ellipse") || tag.equals("ellipse");
    }

    /*
      *
      */
    private boolean isLine(String tag) {
        return tag.equals("svg:line") || tag.equals("line");
    }

    /*
      *
      */
    private boolean isPolyline(String tag) {
        return tag.equals("svg:polyline") || tag.equals("polyline");
    }

    /*
      *
      */
    private boolean isCircle(String tag) {
        return tag.equals("svg:circle") || tag.equals("circle");
    }

    /*
      *
      */
    private boolean isPolygon(String tag) {
        return tag.equals("svg:polygon") || tag.equals("polygon");
    }

    private boolean isGroup(String tag) {
        return tag.equals("svg:g") || tag.equals("g");
    }

    protected class svgShape {
        public Shape shape;

        /**
         * Contains an array of key, value pairs that represent the style of the
         * cell.
         */
        protected Map<String, Object> style;

        public List<svgShape> subShapes;

        /**
         * Holds the current value to which the shape is scaled in X
         */
        protected double currentXScale;

        /**
         * Holds the current value to which the shape is scaled in Y
         */
        protected double currentYScale;

        public svgShape(Shape shape, Map<String, Object> style) {
            this.shape = shape;
            this.style = style;
            subShapes = new ArrayList<svgShape>();
        }

        public double getCurrentXScale() {
            return currentXScale;
        }

        public void setCurrentXScale(double currentXScale) {
            this.currentXScale = currentXScale;
        }

        public double getCurrentYScale() {
            return currentYScale;
        }

        public void setCurrentYScale(double currentYScale) {
            this.currentYScale = currentYScale;
        }
    }

    /**
     * Returns the stylenames in a style of the form stylename[;key=value] or an
     * empty array if the given style does not contain any stylenames.
     *
     * @param style String of the form stylename[;stylename][;key=value].
     * @return Returns the stylename from the given formatted string.
     */
    protected static Map<String, Object> getStylenames(String style) {
        if (style != null && style.length() > 0) {
            Map<String, Object> result = new Hashtable<String, Object>();

            if (style != null) {
                String[] pairs = style.split(";");

                for (int i = 0; i < pairs.length; i++) {
                    String[] keyValue = pairs[i].split(":");

                    if (keyValue.length == 2) {
                        result.put(keyValue[0].trim(), keyValue[1].trim());
                    }
                }
            }
            return result;
        }

        return null;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIconPath() {
        return iconPath;
    }

    public void setIconPath(String iconPath) {
        this.iconPath = iconPath;
    }

    public Rectangle2D getBoundingBox() {
        return boundingBox;
    }

    public void setBoundingBox(Rectangle2D boundingBox) {
        this.boundingBox = boundingBox;
    }
}